/*************************************************************************
 *
 *   file		: Spell.DBC.cs
 *   copyright		: (C) The WCell Team
 *   email		: info@wcell.org
 *   last changed	: $LastChangedDate: 2009-09-15 06:15:29 +0100 (Tue, 15 Sep 2009) $
 *   last author	: $LastChangedBy: dominikseifert $
 *   revision		: $Rev: 1100 $
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *************************************************************************/

using System;
using System.Collections.Generic;
using System.IO;
using WCell.Constants;
using WCell.Constants.Items;
using WCell.Constants.Spells;
using WCell.Core.DBC;
using WCell.RealmServer.Content;
using WCell.RealmServer.Items;
using WCell.Util;
using WCell.Util.Data;
using WCell.RealmServer.Misc;

namespace WCell.RealmServer.Spells
{
	/// <summary>
	/// Represents a Spell (which -in fact- is any kind of effect or action) in WoW.
	/// 
	/// TODO: Spell-Crafting through XML overrides
	/// </summary>
	public partial class Spell
	{
		[NotPersistent]
		public static DBCReader<DurationEntry, DBCDurationConverter> DBCDurationReader;
		[NotPersistent]
		public static DBCReader<float, DBCRadiusConverter> DBCRadiusReader;
		[NotPersistent]
		public static DBCReader<uint, DBCCastTimeConverter> DBCCastTimeReader;
		[NotPersistent]
		public static DBCReader<SimpleRange, DBCRangeConverter> DBCRangeReader;
		[NotPersistent]
		public static DBCReader<string, DBCMechanicConverter> DBCMechanicReader;
        [NotPersistent]
        public static DBCReader<RuneCostEntry, DBCSpellRuneCostConverter> DBCRuneCostReader;

		internal static void InitDbcs()
		{
			DBCDurationReader = new DBCReader<DurationEntry, DBCDurationConverter>(RealmServerConfiguration.GetDBCFile("SpellDuration.dbc"));
			DBCRadiusReader = new DBCReader<float, DBCRadiusConverter>(RealmServerConfiguration.GetDBCFile("SpellRadius.dbc"));
			DBCCastTimeReader = new DBCReader<uint, DBCCastTimeConverter>(RealmServerConfiguration.GetDBCFile("SpellCastTimes.dbc"));
			DBCRangeReader = new DBCReader<SimpleRange, DBCRangeConverter>(RealmServerConfiguration.GetDBCFile("SpellRange.dbc"));
			//DBCMechanicReader = new DBCReader<SpellMechanic, DBCMechanicConverter>(RealmServerConfiguration.GetDBCFile("SpellMechanic.dbc"));
			DBCMechanicReader = new DBCReader<string, DBCMechanicConverter>(RealmServerConfiguration.GetDBCFile("SpellMechanic.dbc"));
		    DBCRuneCostReader = new DBCReader<RuneCostEntry, DBCSpellRuneCostConverter>(RealmServerConfiguration.GetDBCFile("SpellRuneCost.dbc"));
		}

		#region SpellDuration.dbc
		public struct DurationEntry
		{
			public int Min;
			/// <summary>
			/// The amount the duration increases per caster-level
			/// </summary>
			public int LevelDelta;
			public int Max;

			public int Random()
			{
				return Utility.Random(Min, Max);
			}
		}

		public class DBCDurationConverter : DBCRecordConverter<DurationEntry>
		{
			public override DurationEntry ConvertTo(byte[] rawData, ref int id)
			{
				var durations = new DurationEntry();

				id = (int)GetUInt32(rawData, 0);
				durations.Min = GetInt32(rawData, 1);
				durations.LevelDelta = GetInt32(rawData, 2);
				durations.Max = GetInt32(rawData, 3);

				return durations;
			}
		}
		#endregion

		#region SpellRadius.dbc
		public class DBCRadiusConverter : DBCRecordConverter<float>
		{
			public override float ConvertTo(byte[] rawData, ref int id)
			{
				id = (int)GetUInt32(rawData, 0);
				return GetFloat(rawData, 1);
			}
		}
		#endregion

		#region SpellCastTimes.dbc
		public class DBCCastTimeConverter : DBCRecordConverter<uint>
		{
			public override uint ConvertTo(byte[] rawData, ref int id)
			{
				id = (int)GetUInt32(rawData, 0);
				return GetUInt32(rawData, 1);
			}
		}
		#endregion

		#region SpellRange.dbc
		public class DBCRangeConverter : DBCRecordConverter<SimpleRange>
		{
			public override SimpleRange ConvertTo(byte[] rawData, ref int id)
			{
				var range = new SimpleRange();

				id = GetInt32(rawData, 0);
				range.MinDist = (uint)GetFloat(rawData, 1);
				// another min ?
				range.MaxDist = (uint)GetFloat(rawData, 3);
				// another max?

				return range;
			}
		}
		#endregion

		#region SpellMechanic.dbc
		public class DBCMechanicConverter : DBCRecordConverter<string>
		{
			public override string ConvertTo(byte[] rawData, ref int id)
			{
				id = GetInt32(rawData, 0);
				return GetString(rawData, 1);
			}
		}
		#endregion

		#region SpellFocusObject.dbc
		public struct SpellFocusEntry
		{
			public uint Id;
			public string Name;
		}

		public class DBCSpellFocusConverter : DBCRecordConverter<SpellFocusEntry>
		{
			public override SpellFocusEntry ConvertTo(byte[] rawData, ref int id)
			{
			    var entry = new SpellFocusEntry
			                    {
			                        Id = (uint) (id = GetInt32(rawData, 0)),
			                        Name = GetString(rawData, 1)
			                    };

			    return entry;
			}
		}
		#endregion

        #region SpellRuneCost.dbc
        public class RuneCostEntry
        {
            public uint Id;                                                                              
            public uint BloodCost;
            public uint FrostCost;
            public uint UnholyCost;
            public uint PowerGain;                                  
        }

        public class DBCSpellRuneCostConverter : DBCRecordConverter<RuneCostEntry>
        {
            public override RuneCostEntry ConvertTo(byte[] rawData, ref int id)
            {
                var entry = new RuneCostEntry
                                {
                                    Id = (uint) (id = GetInt32(rawData, 0)),
                                    BloodCost = (uint) (GetInt32(rawData, 1)),
                                    FrostCost = (uint) (GetInt32(rawData, 2)),
                                    UnholyCost = (uint) (GetInt32(rawData, 3)),
                                    PowerGain = (uint) (GetInt32(rawData, 4))
                                };
                return entry;
            }
        }
        #endregion

		#region Spell.DBC
		public class Converter : DBCRecordConverter<Spell>
		{
			public override Spell ConvertTo(byte[] rawData, ref int id)
			{
				id = GetInt32(rawData, 0);

				#region Parsing
				uint currentIndex = 0;

				var spell = new Spell {
					Id = GetUInt32(rawData, currentIndex++),
					SpellId = (SpellId)id
				};

				try
				{
					spell.Category = GetUInt32(rawData, currentIndex++);
					spell.DispelType = (DispelType)GetUInt32(rawData, currentIndex++);
					spell.Mechanic = (SpellMechanic)GetUInt32(rawData, currentIndex++);
					spell.Attributes = (SpellAttributes)GetUInt32(rawData, currentIndex++);
					spell.AttributesEx = (SpellAttributesEx)GetUInt32(rawData, currentIndex++);
					spell.AttributesExB = (SpellAttributesExB)GetUInt32(rawData, currentIndex++);
					spell.AttributesExC = (SpellAttributesExC)GetUInt32(rawData, currentIndex++);
					spell.AttributesExD = (SpellAttributesExD)GetUInt32(rawData, currentIndex++);
					spell.AttributesExE = (SpellAttributesExE)GetUInt32(rawData, currentIndex++);
					spell.AttributesExF = (SpellAttributesExF)GetUInt32(rawData, currentIndex++);
                    spell.Unk_322_1 = GetUInt32(rawData, currentIndex++);
					spell.ShapeshiftMask = (ShapeShiftMask)GetUInt32(rawData, currentIndex++);
                    spell.Unk_322_2 = GetUInt32(rawData, currentIndex++);
					spell.ExcludeShapeshiftMask = (ShapeShiftMask)GetUInt32(rawData, currentIndex++);
                    spell.Unk_322_3 = GetUInt32(rawData, currentIndex++);
					spell.TargetFlags = (SpellTargetFlags)GetUInt32(rawData, currentIndex++);

					spell.TargetCreatureTypes = (TargetCreatureMask)GetUInt32(rawData, currentIndex++);
					spell.RequiredSpellFocus = (SpellFocus)GetUInt32(rawData, currentIndex++);
					spell.FacingFlags = (SpellFacingFlags)GetUInt32(rawData, currentIndex++);
					spell.CasterAuraState = (AuraState)GetUInt32(rawData, currentIndex++);
					spell.TargetAuraState = (AuraState)GetUInt32(rawData, currentIndex++);
					spell.ExcludeCasterAuraState = (AuraState)GetUInt32(rawData, currentIndex++);
					spell.ExcludeTargetAuraState = (AuraState)GetUInt32(rawData, currentIndex++);

					spell.CasterAuraId = GetUInt32(rawData, currentIndex++);
					spell.TargetAuraId = GetUInt32(rawData, currentIndex++);

					spell.ExcludeCasterAuraId = GetUInt32(rawData, currentIndex++);
					spell.ExcludeTargetAuraId = GetUInt32(rawData, currentIndex++);


					int castTimeIndex = GetInt32(rawData, currentIndex++);
					if (castTimeIndex > 0)
					{
						if (!DBCCastTimeReader.Entries.TryGetValue(castTimeIndex, out spell.CastDelay))
						{
							ContentHandler.OnInvalidClientData("DBC Spell \"{0}\" referred to invalid CastTime-Entry: {1}", spell.Name, castTimeIndex);
						}
					}

					spell.CooldownTime = Math.Max(0, GetInt32(rawData, currentIndex++) - (int)spell.CastDelay);
					spell.categoryCooldownTime = GetInt32(rawData, currentIndex++);
					spell.InterruptFlags = (InterruptFlags)GetUInt32(rawData, currentIndex++);
					spell.AuraInterruptFlags = (AuraInterruptFlags)GetUInt32(rawData, currentIndex++);
					spell.ChannelInterruptFlags = (ChannelInterruptFlags)GetUInt32(rawData, currentIndex++);
					spell.ProcTriggerFlags = (ProcTriggerFlags)GetUInt32(rawData, currentIndex++);
					spell.ProcChance = GetUInt32(rawData, currentIndex++);
					spell.ProcCharges = GetInt32(rawData, currentIndex++);
					spell.MaxLevel = GetInt32(rawData, currentIndex++);
					spell.BaseLevel = GetInt32(rawData, currentIndex++);
					spell.Level = GetInt32(rawData, currentIndex++);

					var durationIndex = GetInt32(rawData, currentIndex++);
					if (durationIndex > 0)
					{
						if (!DBCDurationReader.Entries.TryGetValue(durationIndex, out spell.Durations))
						{
							ContentHandler.OnInvalidClientData("DBC Spell \"{0}\" referred to invalid Duration-Entry: {1}", spell.Name, durationIndex);
						}
					}

					spell.PowerType = (PowerType)GetUInt32(rawData, currentIndex++);
					spell.PowerCost = GetUInt32(rawData, currentIndex++);
					spell.PowerCostPerlevel = GetUInt32(rawData, currentIndex++);
					spell.PowerPerSecond = GetUInt32(rawData, currentIndex++);
					spell.PowerPerSecondPerLevel = GetUInt32(rawData, currentIndex++);

					var rangeIndex = GetInt32(rawData, currentIndex++);
					if (rangeIndex > 0)
					{
						if (!DBCRangeReader.Entries.TryGetValue(rangeIndex, out spell.Range))
						{
							ContentHandler.OnInvalidClientData("DBC Spell \"{0}\" referred to invalid Range-Entry: {1}", spell.Name, rangeIndex);
						}
					}

					spell.ProjectileSpeed = GetFloat(rawData, currentIndex++);
					spell.ModalNextSpell = (SpellId)GetUInt32(rawData, currentIndex++);

					spell.MaxStackCount = GetInt32(rawData, currentIndex++);

					spell.RequiredToolIds = new uint[2];
					for (var i = 0; i < spell.RequiredToolIds.Length; i++)
					{
						spell.RequiredToolIds[i] = GetUInt32(rawData, currentIndex++);
					}

					List<ItemStackDescription> reagents = null;
					uint reagentStart = currentIndex;
					for (int i = 0; i < 8; i++)
					{
						ReadReagent(rawData, reagentStart, i, out currentIndex, ref reagents);
					}
					if (reagents != null)
					{
						spell.Reagents = reagents.ToArray();
					}
					else
					{
						spell.Reagents = ItemStackDescription.EmptyArray;
					}

					spell.RequiredItemClass = (ItemClass)GetUInt32(rawData, currentIndex++);
					if (spell.RequiredItemClass < 0)
					{
						spell.RequiredItemClass = ItemClass.None;
					}

					spell.RequiredItemSubClassMask = (ItemSubClassMask)GetUInt32(rawData, currentIndex++);
					if (spell.RequiredItemSubClassMask < 0)
					{
						spell.RequiredItemSubClassMask = ItemSubClassMask.None;
					}

					spell.RequiredItemInventorySlotMask = (InventorySlotMask)GetUInt32(rawData, currentIndex++);
					if (spell.RequiredItemInventorySlotMask < 0)
					{
						spell.RequiredItemInventorySlotMask = InventorySlotMask.None;
					}

					var effects = new List<SpellEffect>(3);
					uint effectStart = currentIndex;
					for (int i = 0; i < 3; i++)
					{
						SpellEffect effect = ReadEffect(spell, rawData, effectStart, i, out currentIndex);
						if (effect != null && effect.EffectType != SpellEffectType.None)
						{
							effects.Add(effect);
						}
					}
					spell.Effects = effects.ToArray();

					spell.Visual = GetUInt32(rawData, currentIndex++);
					spell.Visual2 = GetUInt32(rawData, currentIndex++);
					spell.SpellbookIconId = GetUInt32(rawData, currentIndex++);
					spell.BuffIconId = GetUInt32(rawData, currentIndex++);
					spell.Priority = GetUInt32(rawData, currentIndex++);

				    spell.Name = GetString(rawData, ref currentIndex);
				    spell.RankDesc = GetString(rawData, ref currentIndex);
					//++currentIndex;														// unknown
                    spell.Description = GetString(rawData, ref currentIndex);
                    spell.BuffDescription = GetString(rawData, ref currentIndex);

					spell.PowerCostPercentage = GetUInt32(rawData, currentIndex++);
					spell.StartRecoveryCategory = GetInt32(rawData, currentIndex++);
					spell.StartRecoveryTime = GetInt32(rawData, currentIndex++);
					spell.MaxTargetLevel = GetUInt32(rawData, currentIndex++);
					spell.SpellClassSet = (SpellClassSet)GetUInt32(rawData, currentIndex++);

					spell.SpellClassMask[0] = GetUInt32(rawData, currentIndex++);
					spell.SpellClassMask[1] = GetUInt32(rawData, currentIndex++);
					spell.SpellClassMask[2] = GetUInt32(rawData, currentIndex++);

					spell.MaxTargets = GetUInt32(rawData, currentIndex++);
					spell.DefenseType = (SpellDefenseType)GetUInt32(rawData, currentIndex++);
					spell.PreventionType = (SpellPreventionType)GetUInt32(rawData, currentIndex++);
					spell.StanceBarOrder = GetInt32(rawData, currentIndex++);

					spell.DamageMultipliers = new float[3];
					for (int i = 0; i < spell.DamageMultipliers.Length; i++)
					{
						spell.DamageMultipliers[i] = GetFloat(rawData, currentIndex++);
					}

					spell.MinFactionId = GetUInt32(rawData, currentIndex++);
					spell.MinReputation = GetUInt32(rawData, currentIndex++);
					spell.RequiredAuraVision = GetUInt32(rawData, currentIndex++);

					spell.RequiredTotemCategories = new TotemCategory[2];
					for (int i = 0; i < spell.RequiredTotemCategories.Length; i++)
					{
						spell.RequiredTotemCategories[i] = (TotemCategory)GetUInt32(rawData, currentIndex++);
					}

					spell.AreaGroupId = GetUInt32(rawData, currentIndex++);
					spell.SchoolMask = (DamageSchoolMask)GetUInt32(rawData, currentIndex++);

                    spell.RuneCostId = GetUInt32(rawData, currentIndex++);
					spell.MissileId = GetUInt32(rawData, currentIndex++);

                    // New 3.1.0. Id from PowerDisplay.dbc
				    spell.PowerDisplayId = GetInt32(rawData, currentIndex++);

                    // 3.2.2 unk float (array?)
                    spell.Unk_322_4_1 = GetUInt32(rawData, currentIndex++);
                    spell.Unk_322_4_2 = GetUInt32(rawData, currentIndex++);
                    spell.Unk_322_4_3 = GetUInt32(rawData, currentIndex++);

                    // 3.2.2
                    spell.spellDescriptionVariablesID = GetUInt32(rawData, currentIndex++);
				}
				catch (Exception e)
				{
					throw new Exception(string.Format("Unable to parse Spell from DBC file. Index: " + currentIndex), e);
				}
				#endregion

				return spell;
			}

			private void ReadReagent(byte[] rawData, uint reagentStart, int reagentNum, out uint currentIndex, ref List<ItemStackDescription> list)
			{
				currentIndex = (uint)(reagentStart + reagentNum);
				var id = (ItemId)GetUInt32(rawData, currentIndex);
				currentIndex += 8;
				var count = GetUInt32(rawData, currentIndex);
				currentIndex += (uint)(8 - reagentNum);

				if (id > 0 && count > 0)
				{
					if (list == null)
					{
						list = new List<ItemStackDescription>();
					}
					var reagent = new ItemStackDescription {ItemId = id, Amount = count};
				    list.Add(reagent);
				}
			}

			private SpellEffect ReadEffect(Spell spell, byte[] rawData, uint effectStartIndex, int effectNum, out uint currentIndex)
			{
				var effect = new SpellEffect(spell, effectNum);

				currentIndex = (uint)(effectStartIndex + effectNum);

				effect.EffectType = (SpellEffectType)GetUInt32(rawData, currentIndex);
				currentIndex += 3;

				effect.DiceSides = GetInt32(rawData, currentIndex);
				currentIndex += 3;

				effect.DiceCount = GetInt32(rawData, currentIndex);
				currentIndex += 3;

				effect.DiePerLevel = GetFloat(rawData, currentIndex);
				currentIndex += 3;

				effect.RealPointsPerLevel = GetFloat(rawData, currentIndex);
				currentIndex += 3;

				effect.BasePoints = GetInt32(rawData, currentIndex);
				currentIndex += 3;

				effect.Mechanic = (SpellMechanic)GetUInt32(rawData, currentIndex);
				currentIndex += 3;

				effect.ImplicitTargetA = (ImplicitTargetType)GetUInt32(rawData, currentIndex);
				currentIndex += 3;

				effect.ImplicitTargetB = (ImplicitTargetType)GetUInt32(rawData, currentIndex);
				currentIndex += 3;

				// Fix: This is a default AoE effect, thus doesn't have a fact at destination
				if (effect.ImplicitTargetA == ImplicitTargetType.AllEnemiesAroundCaster &&
					effect.ImplicitTargetB == ImplicitTargetType.AllEnemiesInArea)
				{
					effect.ImplicitTargetB = ImplicitTargetType.None;
				}

				int radiusIndex = GetInt32(rawData, currentIndex);
				if (radiusIndex > 0)
				{
					DBCRadiusReader.Entries.TryGetValue(radiusIndex, out effect.Radius);
				}
				//if (effect.Radius < 1) {
				//    effect.Radius = 5;
				//}
				currentIndex += 3;

				effect.AuraType = (AuraType)GetUInt32(rawData, currentIndex);
				currentIndex += 3;

				effect.Amplitude = GetUInt32(rawData, currentIndex);
				currentIndex += 3;

				effect.ProcValue = GetFloat(rawData, currentIndex);
				currentIndex += 3;

				effect.ChainTargets = GetInt32(rawData, currentIndex);
				currentIndex += 3;

				effect.ItemId = GetUInt32(rawData, currentIndex);
				currentIndex += 3;

				effect.MiscValue = GetInt32(rawData, currentIndex);
				currentIndex += 3;

				effect.MiscValueB = GetInt32(rawData, currentIndex);
				currentIndex += 3;

				effect.TriggerSpellId = (SpellId)GetUInt32(rawData, currentIndex);
				currentIndex += 3;

				effect.PointsPerComboPoint = GetFloat(rawData, currentIndex);
				currentIndex += (uint)(3 - effectNum);


				// since the masks are stored congruently instead of indexed
				currentIndex += (uint)(effectNum * 3);

				effect.AffectMask[0] = GetUInt32(rawData, currentIndex++);
				effect.AffectMask[1] = GetUInt32(rawData, currentIndex++);
				effect.AffectMask[2] = GetUInt32(rawData, currentIndex++);

				// skip ahead 6 for index 0, 3 for index 1, and 0 for index 2
				currentIndex += (uint)((2 - effectNum) * 3);

				return effect;
			}
		}

		#endregion

		public SpellEffect GetAuraEffect(AuraType aura)
		{
			foreach (SpellEffect effect in Effects)
			{
				if (effect.AuraType == aura)
				{
					return effect;
				}
			}
			return null;
		}

		#region Verbose / Debug
		public void PrintEffects(TextWriter writer)
		{
			foreach (SpellEffect effect in Effects)
			{
				effect.DumpInfo(writer, "");
			}
		}
		#endregion
	}
}